package jmpo.strm;

import java.util.List;
import java.util.stream.Stream;

/**
 * Stream API - это специальные возможности Java 8 для работы с потоками данных.
 * Потоки данных позволяют последовательно получать данные, обрабатывать их и 
 * производить конечные операции. Отсюда происходят названия трёх стадий жизни
 * любого потока даных:
 *     source       - источник данных (им может быть коллекция, файл, сетевое соединение, генератор и т.п.)
 *     intermediate - операции преобразования данных от одного источника к другому
 *     terminal     - операции обработки данных к результату: 
 *          редукция к одному элементу или обработка с побочным эффектом
 * 
 * Таким образом для получения конечного результата при обработке данных нам всегда 
 * будут нужны эти три компонента. Важно отметить, что потоки, в отличие от Collection API, 
 * являются <<ленивыми>> (lazy) или <<нестрогими>> (nonstrict execution). Т.е. если мы создали
 * источник данных, это вовсе не означает, что мы их действительно прочитали в память; если мы 
 * произвели операцию фильтрации над источником, это не означает, что данные действительно были
 * отфильтрованы и где-либо сохранены. Только терминальные операции действительно запускают 
 * процесс обработки! 
 */
final class StreamExample {
    public static void test() {
        List<String> names = List.of("Ann", "Bill", "Rod", "Angel", "Helga", "Alen", "Sam");
        {
            /**
             * Создаём поток, в котором источником является коллекция
             */
            Stream<String> strstream = names.stream();

            /**
             * Теперь мы добавляем в качестве последующей операции преобразование строки в символ.
             * Действия реально не выполняются!
             * Мы выражаем лишь своё намерение над каждой строкой в исходном потоке данных 
             * произвести указанную операцию. В результате мы получаем не конечный ответ в 
             * виде набора символов, а новый поток данных.
             */
            Stream<Character> charstream = strstream.map( s -> s.charAt(0));

            /**
             * Теперь мы обработаем наш поток данных.
             * Подготовим объект, который будет принимать наши данные.
             */
            final StringBuffer sb = new StringBuffer(); // stream api допускает внутреннее распараллеливание, объект приёмник должен быть потокобезопасным

            /**
             * Используем терминальный forEach, выполняющий операцию над каждым элементом.
             */
            charstream.forEach(c -> sb.append(c));

            /**
             * Теперь наш stream полностью обработан! 
             * Причём и источник, и преобразованный stream. Попытка для любого из них вызвать операцию
             * приведёт к исключению, так как поток уже находится в особом состоянии. 
             * Данные из потока можно потребить только один раз! Так же как и использование Iterator - 
             * его можно обойти методом next только один раз, далее нужно брать новый iterator.
             */
            System.out.println(sb.toString());
        }

        /**
         * На самом деле нам не нужно отдельно сохранять промежуточные потоки, мы можем всё записать 
         * в одно выражение. Код станет коротким и ёмким.
         */
        {
            final StringBuffer sb2 = new StringBuffer();
            names.stream().map(s -> s.charAt(0)).forEach(c -> sb2.append(c));
            System.out.println(sb2.toString());
        }

        /**
         * Создавать отдельно StringBuffer, а затем использовать forEach не самый удобный способ.
         * Stream API поддерживает для этих целей специальный терминальный метод collect.
         */
        {
            System.out.println(
                names.stream().map(s -> s.charAt(0)).collect(
                    StringBuilder::new,    // вызывается новым потоком, чтобы создать объект-аккумулятор
                    StringBuilder::append, // вызывается потоком на аккумуляторе для компоновки результата
                    StringBuilder::append  // вызывается на двух объектах аккумуляторах, чтобы объединить результаты
                )
                .toString()
            );
        }
    }
}

/**
 * Важно понимать, что Stream API позволяет производить выполнение параллельно, в несколько потоков.
 * Такой подход может дать существенное ускорение при обработке данных.
 * Для того, чтобы подсказать JVM, что выхотите параллельное исполнение можно использовать метод parallel, 
 * вызвав его на потоке.
 * При создании поток данных получает некоторые свойства, он может быть упорядоченным или не упорядоченным.
 * Если поток упорядочен, то при параллельном выполнении он будет ожидать синхронизации, чтобы передавать 
 * данные внужном порядке. Когда порядок не важен, то можно ускорить выполнение, подсказав вызовом unordered,
 * что порядок данных в выходном потоке можно не соблюдать и не синхронизировать операции.
 * не имеет значения. И наооброт, если в какой-то момент обработка должна осуществляться строго в известном 
 * порядке, например поток был отсортирован и вам важен порядок элементов, то следует использовать вызов 
 * sequential, который вернётся к поведению по умолчанию (порядок результата такой же как и входящего потока).
 */
final class OrderedUnorderedExample {
    public static void test() {
        var strstream = List.of("Set", "Get", "Swap", "Map", "Let", "Bet", "Cat", "Add", "Rat", "Sad", "Led", "Red", "Pet", "Act", "Art", "Fat", "Dad").stream();

        strstream.parallel().unordered().map(s -> s + s).sorted().sequential().forEach(System.out::println);
    }
}

/**
 * Основой работы Stream является интерфейс Spliterator, который выполняет роль итератора в потоке данных.
 * Его основные операции
 *     boolean tryAdvance(Consumer<? super T> c) - подобие метода next, но он не возвращает значение, а передаёт
 *     значение в реализацию Consumer, который производит нужное действие; если элемент был передан потребителю, 
 *     то метод возвращает true, иначе - false
 * 
 *     Spliterator<T> trySplit() - этот метод вызывается, когда система пытается выполнить задачи нашего Spliterator
 *     параллельно, если часть задач можно сделать одновременно, то метод должен вернуть Spliterator на эту часть 
 *     задач и гарантировать, что они не будут обработаны им самим. Если разделение невозможно, то следует 
 *     вернуть null.
 * 
 *     long estimateSize() - этот метод должен вернуть размер потока данных, важно, что размер потока данных может
 *     быть бесконечным или сложно вычислимым, или неизвестен, тогда метод может вернуть Long.MAX_VALUE; в отличие
 *     от метода size для коллекций, estimateSize для Spliterator допускает размер long, что позволяет обработать 
 *     в квадрат раз больше данных.
 * 
 *     int characteristics() - возвращает битовую маску характеристик потока данных
 * 
 * Характеристики потока данных (статические поля интерфейса Spliterator):
 *     CONCURRENT - допускает потокобезопасную модификацию
 *     DISTINCT   - для любых двух элементов в потоке данных верно !x.equals(y)
 *     IMMUTABLE  - поток данных не изменяем (нельзя добавлять, изменять или удалять данные из потока)
 *     NONNULL    - поток не содержит нулевых ссылок
 *     ORDERED    - поток должен сохранять порядок элементов
 *     SIZED      - значение, возвращённое через estimateSize является точным количеством операций tryAdvance,
 *     которые могут быть произведены над потоком без модификации данных
 *     SORTED     - данные соответствуют некоторому порядку
 *     SUBSIZED   - если выполнить trySplit, то результат будет SUBSIZED и SIZED
 * 
 * Некоторые из этих характеристик связаны. Например нельзя одновременно поддерживать COUNCURRENT и IMMUTABLE (
 * бесполезная комбинация), если поток данных SIZED, то он обязательно SUBSIZED (но это нужно указывать явно), 
 * обратное неверно (поток может быть SUBSIZED, но не SIZED).
 * 
 * Несколько характеристик объединяютя в маску операцией побитового или:
 * IMMUTABLE | ORDERED | SORTED | SIZED | SUBSIZED
 */

public class BasicStream {
    public static void main(String[] args) {
        if (false) StreamExample.test();
        if (false) OrderedUnorderedExample.test();
    }
}
